TypeScript の Branded Type について少し学ぶ
たとえば URL のエンコード。次の encode() 関数は、まだエンコードしてない文字列だけを受け取りたいとする。
code:ts
const encode(decoded: string) => encodeURIComponent(decoded);
しかし decoded はただの文字列でしかない
エンコード済みの文字列も渡すことができてしまい、多重エンコードが起きてしまう
未エンコードの文字列のみを受け取るように、コンパイラで検出できるようにしたい
code:scala
class StrT (val str: String) trait Normal
trait Encoded
のように記述できる
内部では利用されない型パラメータ(Normal, Encoded) を使って、Str[T] にはそういう種類があることを表現できる
また、Str[Normal] と Str[Encoded] は異なる型であるために互いに assign できないことも表現している
code:ts
interface Str<T> { val: string; }; // Scala のマネ
let a: Str<Encoded>;
let b: Str<Normal>;
const encode = (s: Str<Normal>) => { ... };
encode(a); // OK
code:ts
interface ISO8601 extends String { __ISO8601: never };
const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|+-\d{2}:\d{2})$/; export const iso8601 = (str: string): ISO8601 => {
if (pattern.test(str) throw Error("Not ISO8601");
return str as ISO8601;
};
iso("2026-02-17");
extends String が少しキモい
そこで string との intersection 型によって表現すると
code:ts
type ISO8601 = string & { __ISO8601: never };
const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|+-\d{2}:\d{2})$/; export const iso8601 = (str: string): ISO8601 => {
if (pattern.test(str) throw Error("Not ISO8601");
return str as ISO8601;
};
iso("2026-02-17");